/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.rpc.http;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.openislands.oi.constant.Dict;
import org.openislands.oi.rpc.base.*;
import org.openislands.oi.util.ConfiguratorUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.*;

@Component
public class RequestHandlerContext implements ApplicationListener<ContextRefreshedEvent> {
    private static final Logger log = LoggerFactory.getLogger(RequestHandlerContext.class);

    /***
     * HttpMethod corresponding HttpRequestHandler Cache
     * Load see {@link #onApplicationEvent(ContextRefreshedEvent)}
     */
    private final Map<HttpMethod, HttpRequestHandler> requestHandlerCache = new HashMap<>();

    /***
     * Component cache that implements HttpRequestListener
     * Load see {@link #onApplicationEvent(ContextRefreshedEvent)}
     */
    private final List<HttpRequestListener> requestListenerCache = new ArrayList<>();

    /***
     * Component cache that implements HttpResponseListener
     * Load see {@link #onApplicationEvent(ContextRefreshedEvent)}
     */
    private final List<HttpResponseListener> responseListenerCache = new ArrayList<>();

    /***
     * Policy when route scheduling fails
     */
    private static RoutePolicy routePolicy = new RoutePolicy.DefaultRoutePolicy();

    @SuppressWarnings("all")
    public Object request(RequestMethod requestMethod, Executor<?> executor) throws IOException {
        Executor<Object> httpExecutor = (HttpExecutorProxy.HttpExecutor) executor;
        ItemExtended httpExtended = executor.extended().get(HttpExtended.class, new HttpExtended());
        // add listener
        RequestCallbackChain requestCallbackChain = new RequestCallbackChain(httpExecutor);
        requestCallbackChain.addAll(this.requestListenerCache);
        ResponseCallbackChain responseCallbackChain = new ResponseCallbackChain(httpExecutor);
        httpExtended.putItem(HttpExtended.REQUEST_LISTENER, requestCallbackChain);
        responseCallbackChain.addAll(this.responseListenerCache);
        httpExtended.putItem(HttpExtended.RESPONSE_LISTENER, responseCallbackChain);
        // send request
        HttpMethod httpMethod = HttpMethod.resolve(requestMethod.name());
        HttpRequestHandler httpRequestHandler = this.requestHandlerCache.get(httpMethod);
        if (Objects.nonNull(httpRequestHandler)) {
            return httpRequestHandler.handle(executor);
        }
        throw new RuntimeException(String.format("httpMethod %s HttpRequestHandler undefined", httpMethod));
    }

    /**
     * Handle an application event.
     *
     * @param event the event to respond to
     */
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        ApplicationContext applicationContext = event.getApplicationContext();
        Map<String, HttpRequestHandler> handlerOfType = applicationContext.getBeansOfType(HttpRequestHandler.class);
        for (Map.Entry<String, HttpRequestHandler> entry : handlerOfType.entrySet()) {
            this.requestHandlerCache.put(entry.getValue().method(), entry.getValue());
        }

        Map<String, HttpRequestListener> requestOfType = applicationContext.getBeansOfType(HttpRequestListener.class);
        this.requestListenerCache.addAll(requestOfType.values());

        Map<String, HttpResponseListener> responseOfType = applicationContext.getBeansOfType(HttpResponseListener.class);
        this.responseListenerCache.addAll(responseOfType.values());

        Map<String, RoutePolicy> routePolicyOfType = applicationContext.getBeansOfType(RoutePolicy.class);
        if (!routePolicyOfType.isEmpty())
            RequestHandlerContext.routePolicy = routePolicyOfType.values().iterator().next();
    }

    @SuppressWarnings("unused")
    @Bean("restTemplate")
    public RestTemplate restTemplate(@Value(Dict.VALUE_REST_READ_TIMEOUT) Integer readTimeout,
                                     @Value(Dict.VALUE_REST_CONNECT_TIMEOUT) Integer connectTimeout) {
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setReadTimeout(readTimeout);
        requestFactory.setConnectTimeout(connectTimeout);
        return new RestTemplate(requestFactory);
    }

    @SuppressWarnings("unused")
    @Bean("postHttpRequestHandler")
    public HttpRequestHandler postHttpRequestHandler(RestTemplate restTemplate) {
        return RequestHandlerContext.getHttpRequestHandler(HttpMethod.POST, restTemplate);
    }

    @SuppressWarnings("unused")
    @Bean("deleteHttpRequestHandler")
    public HttpRequestHandler deleteHttpRequestHandler(RestTemplate restTemplate) {
        return RequestHandlerContext.getHttpRequestHandler(HttpMethod.DELETE, restTemplate);
    }

    @SuppressWarnings("unused")
    @Bean("putHttpRequestHandler")
    public HttpRequestHandler putHttpRequestHandler(RestTemplate restTemplate) {
        return RequestHandlerContext.getHttpRequestHandler(HttpMethod.PUT, restTemplate);
    }

    @SuppressWarnings("unused")
    @Bean("getHttpRequestHandler")
    public HttpRequestHandler getHttpRequestHandler(RestTemplate restTemplate) {
        return RequestHandlerContext.getHttpRequestHandler(HttpMethod.GET, restTemplate);
    }

    @Bean("patchHttpRequestHandler")
    public HttpRequestHandler patchHttpRequestHandler(RestTemplate restTemplate) {
        return RequestHandlerContext.getHttpRequestHandler(HttpMethod.PATCH, restTemplate);
    }

    public static RequestCallback getDefaultCallback(RestTemplate restTemplate, Class<?> responseClass, Object requestBody) {
        if (Objects.isNull(requestBody)) {
            return restTemplate.acceptHeaderRequestCallback(responseClass);
        } else {
            return restTemplate.httpEntityCallback(requestBody, responseClass);
        }
    }

    public static HttpRequestHandler getHttpRequestHandler(HttpMethod httpMethod, RestTemplate restTemplate) {
        return new HttpRequestHandler() {
            @Override
            public Object handle(Executor<?> executor) throws IOException {
                Extended<Class<?>, ItemExtended> extended = executor.extended();
                ItemExtended baseExtended = extended.get(BaseExtended.class);
                ItemExtended httpExtended = extended.get(HttpExtended.class);

                RouteNode routeNode = baseExtended.getElement(BaseExtended.ROUTE);
                Type responseType = baseExtended.getElement(BaseExtended.RESPONSE_TYPE);
                String contentPath = ConfiguratorUtils.contextPath(baseExtended.getElement(BaseExtended.SERVICE_MARK));
                String urlPath = httpExtended.getElement(HttpExtended.URL_PATH, "");
                Object requestBody = httpExtended.getElement(HttpExtended.REQUEST_BODY);
                Class<?> responseClass = httpExtended.getElement(HttpExtended.RESPONSE_CLASS);
                Map<String, Object> uriVariables = httpExtended.getElement(HttpExtended.URI_VARIABLE);
                RequestCallbackChain requestCallback = httpExtended.getElement(HttpExtended.REQUEST_LISTENER);
                ResponseCallbackChain responseExtractor = httpExtended.getElement(HttpExtended.RESPONSE_LISTENER);

                // default request callback
                final RequestCallback callback = getDefaultCallback(restTemplate, responseClass, requestBody);
                requestCallback.add((exec, clientHttpRequest) -> {
                    callback.doWithRequest(clientHttpRequest);
                    return null;
                });

                // default response parser
                JSONObjectParser parser = new JSONObjectParser(responseType != null ? responseType : responseClass);
                responseExtractor.add((exec, response) -> parser.extractData(response));

                RouteNode route = routeNode;
                while (route != null) {
                    String url = "http://" + route.getHttpAccess() + contentPath + urlPath;
                    try {
                        log.info("send request method: {}, url: {}.", this.method(), url);
                        return restTemplate.execute(url, this.method(), requestCallback, responseExtractor, uriVariables);
                    } catch (RestClientException e) {
                        log.warn("request url: {} failed. e: {}", url, e);
                        RouteNode nextRoute = route.getNextNode();
                        route = routePolicy.executionFailed(executor, route, nextRoute);
                    }
                }
                return routePolicy.allExecutionFailed(executor);
            }

            @Override
            public HttpMethod method() {
                return httpMethod;
            }
        };
    }

    // Read the request body json from HttpResponse and parse it into an object corresponding to type
    public static class JSONObjectParser implements ResponseExtractor<Object> {
        // json parse type
        private Type type;

        // json parse type reference
        private TypeReference<?> typeReference;

        public JSONObjectParser(Type type) {
            this.type = type;
        }

        @SuppressWarnings("unused")
        public JSONObjectParser(TypeReference<?> typeReference) {
            this.typeReference = typeReference;
        }

        @Override
        public Object extractData(ClientHttpResponse response) throws IOException {
            int len;
            final byte[] bytes = new byte[2048];
            final InputStream inputStream = response.getBody();
            final ByteArrayOutputStream bos = new ByteArrayOutputStream();
            while ((len = inputStream.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
            if (Objects.isNull(this.typeReference)) {
                return new ObjectMapper().readValue(bos.toString(), new TypeReference<Void>() {
                    @Override
                    public Type getType() {
                        return type;
                    }
                });
            } else {
                return new ObjectMapper().readValue(bos.toString(), typeReference);
            }
        }
    }
}
